package com.qisimanxiang.dalaran.zuul.filter;

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.EntryType;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.netflix.hystrix.exception.HystrixRuntimeException;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.netflix.zuul.http.ServletInputStreamWrapper;
import com.qisimanxiang.dalaran.zuul.custom.concurrent.ThreadSemaphore;
import com.qisimanxiang.dalaran.zuul.exception.SlaDegradeException;
import com.qisimanxiang.dalaran.zuul.param.PublicParam;
import com.qisimanxiang.dalaran.zuul.sentinel.DegradeStrategy;
import com.qisimanxiang.dalaran.zuul.sentinel.QpsStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.ribbon.support.RibbonCommandContext;
import org.springframework.cloud.netflix.ribbon.support.RibbonRequestCustomizer;
import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommand;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandFactory;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException;
import org.springframework.http.client.ClientHttpResponse;

import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;

import static com.qisimanxiang.dalaran.zuul.util.ContextConstant.PUBLIC_PARAM_KEY;


/**
 * @author wangmeng
 */
@Slf4j
public class CustomRouteFilter extends RibbonRoutingFilter {

    public CustomRouteFilter(ProxyRequestHelper helper, RibbonCommandFactory<?> ribbonCommandFactory, List<RibbonRequestCustomizer> requestCustomizers) {
        super(helper, ribbonCommandFactory, requestCustomizers);
    }

    @Override
    public Object run() {
        RequestContext currentContext = RequestContext.getCurrentContext();
        String contextUri = (String) currentContext.get(FilterConstants.REQUEST_URI_KEY);
        String serviceId = (String) currentContext.get(FilterConstants.SERVICE_ID_KEY);
        PublicParam publicParam = (PublicParam) currentContext.get(PUBLIC_PARAM_KEY);
        contextUri = contextUri == null ? null : contextUri.trim();
        serviceId = serviceId == null ? null : serviceId.trim();


        if (log.isDebugEnabled()) {
            log.info("contextUri:{},serviceId:{},publicParam:{}", contextUri, serviceId, publicParam);
            log.info(" custom route filter running .... ");
        }

        //并发取信号量
        boolean acquire = ThreadSemaphore.tryAcquire(serviceId, contextUri, publicParam.getCustomerId());
        if (!acquire) {
            write500Response(GLOBAL_CONCURRENT_BODY);
            return null;
        }
        try {
            return globalDegradeRun(serviceId, contextUri, publicParam.getCustomerId());
        } finally {
            ThreadSemaphore.release(serviceId, contextUri, publicParam.getCustomerId());
        }
    }

    /**
     * 全局熔断配置
     *
     * @param contextUri 应用PATH
     * @param serviceId  应用ID
     * @return 响应结果
     */
    private Object globalDegradeRun(String serviceId, String contextUri, String customerId) {
        Entry entry = null;
        try {
            entry = SphU.entry(DegradeStrategy.RESOURCE_NAME_CUSTOMER_DEGRADE_SERVICE);
            return globalLimitRun(serviceId, contextUri, customerId);
        } catch (BlockException e) {
            //TODO global block response
            if (!BlockException.isBlockException(e)) {
                Tracer.trace(e);
            }
            write500Response(GLOBAL_DEGRADE_BODY);
        } finally {
            if (entry != null) {
                entry.exit(1);
            }
        }
        return null;
    }

    /**
     * 全局调用限制
     * 注意：若 entry 的时候传入了热点参数，那么 exit 的时候也一定要带上对应的参数（exit(count, args)），否则可能会有统计错误。
     *
     * @param contextUri 应用PATH
     * @param serviceId  应用ID
     * @return 响应结果
     */
    private Object globalLimitRun(String serviceId, String contextUri, String customerId) {
        Entry entry = null;
        try {
            entry = SphU.entry(QpsStrategy.RESOURCE_NAME_GLOBAL, EntryType.IN, 1, serviceId, contextUri);
            return serviceLimitRun(serviceId, contextUri, customerId);
        } catch (BlockException e) {
            //TODO global block response
            write500Response(GLOBAL_BLOCK_BODY);
        } finally {
            if (entry != null) {
                entry.exit(1, serviceId, contextUri);
            }
        }
        return null;
    }


    /**
     * 服务调用限制
     * 注意：若 entry 的时候传入了热点参数，那么 exit 的时候也一定要带上对应的参数（exit(count, args)），否则可能会有统计错误。
     *
     * @param contextUri 应用PATH
     * @param serviceId  应用ID
     * @return 响应结果
     */
    private Object serviceLimitRun(String serviceId, String contextUri, String customerId) {
        Entry entry = null;
        try {
            entry = SphU.entry(QpsStrategy.RESOURCE_NAME_SERVICE, EntryType.IN, 1, serviceId, contextUri);
            return customerServiceLimitRun(serviceId, contextUri, customerId);
        } catch (BlockException e) {
            //TODO service block response
            write500Response(SERVICE_BLOCK_BODY);
        } finally {
            if (entry != null) {
                entry.exit(1, serviceId, contextUri);
            }
        }
        return null;
    }

    /**
     * 客户服务调用限制
     * 注意：若 entry 的时候传入了热点参数，那么 exit 的时候也一定要带上对应的参数（exit(count, args)），否则可能会有统计错误。
     *
     * @param contextUri 应用PATH
     * @param serviceId  应用ID
     * @return 响应结果
     */
    private Object customerServiceLimitRun(String serviceId, String contextUri, String customerId) {
        if (customerId == null) {
            return super.run();
        }
        Entry entry = null;
        try {
            entry = SphU.entry(QpsStrategy.RESOURCE_NAME_CUSTOMER_SERVICE, EntryType.IN, 1, serviceId, contextUri + "::" + customerId);
            return customerServiceTcLimitRun(serviceId, contextUri, customerId);
        } catch (BlockException e) {
            //TODO customer service block response
            write500Response(CUSTOMER_SERVICE_BLOCK_BODY);
        } finally {
            if (entry != null) {
                entry.exit(1, serviceId, contextUri + "::" + customerId);
            }
        }
        return null;
    }

    private Object customerServiceTcLimitRun(String serviceId, String contextUri, String customerId) {
        if (customerId == null) {
            return doRun();
        }
        Entry entry = null;
        try {
            entry = SphU.entry(QpsStrategy.RESOURCE_NAME_CUSTOMER_SERVICE_TC, EntryType.IN, 1, serviceId, contextUri + "-tc-" + customerId);
            return doRun();
        } catch (BlockException e) {
            //TODO customer service block response
            write500Response(CUSTOMER_SERVICE_THREAD_BODY);
        } finally {
            if (entry != null) {
                entry.exit(1, serviceId, contextUri + "-tc-" + customerId);
            }
        }
        return null;
    }


    public Object doRun() {
        RequestContext context = RequestContext.getCurrentContext();
        this.helper.addIgnoredHeaders();
        try {
            RibbonCommandContext commandContext = buildCommandContext(context);
            ClientHttpResponse response = forward(commandContext);
            setResponse(response);
            return response;
        } catch (SlaDegradeException ex) {
            //捕获SLA熔断异常
            write500Response(GLOBAL_SLA_DEGRADE_BODY);
            return null;
        } catch (ZuulException ex) {
            throw new ZuulRuntimeException(ex);
        } catch (Exception ex) {
            throw new ZuulRuntimeException(ex);
        }
    }


    @Override
    protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception {
        Map<String, Object> info = this.helper.debug(context.getMethod(),
                context.getUri(), context.getHeaders(), context.getParams(),
                context.getRequestEntity());

        RibbonCommand command = this.ribbonCommandFactory.create(context);
        try {
            /* SLA熔断会影响效率 */
            ClientHttpResponse response = command.execute();
            if (response == null) {
                write500Response(GLOBAL_SLA_DEGRADE_BODY);
                throw new SlaDegradeException();
            }
            this.helper.appendDebug(info, response.getRawStatusCode(),
                    response.getHeaders());
            return response;
        } catch (HystrixRuntimeException ex) {
            return handleException(info, ex);
        }

    }


    private static final String CONTENT_TYPE = "application/json";
    private static final String GLOBAL_DEGRADE_BODY = "{\"msg\":\"超时熔断\"}";
    private static final String GLOBAL_CONCURRENT_BODY = "{\"msg\":\"并发超限\"}";
    private static final String GLOBAL_SLA_DEGRADE_BODY = "{\"msg\":\"SLA熔断\"}";
    private static final String GLOBAL_BLOCK_BODY = "{\"msg\":\"平台服务能力超限\"}";
    private static final String SERVICE_BLOCK_BODY = "{\"msg\":\"接口服务能力超限\"}";
    private static final String CUSTOMER_SERVICE_BLOCK_BODY = "{\"msg\":\"QPS超限\"}";
    private static final String CUSTOMER_SERVICE_THREAD_BODY = "{\"msg\":\"线程数超限\"}";

    private void write200Response(String msg) {
        writeResponse(msg, HttpServletResponse.SC_OK);
    }

    private void write500Response(String msg) {
        log.info("异常响应：{}", msg);
        writeResponse(msg, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    }

    private void writeResponse(String msg, Integer status) {
        try {
            RequestContext currentContext = RequestContext.getCurrentContext();
            currentContext.setResponseDataStream(new ServletInputStreamWrapper(msg.getBytes(StandardCharsets.UTF_8)));
            HttpServletResponse response = currentContext.getResponse();
            response.setContentType(CONTENT_TYPE);
            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
            currentContext.setResponseBody(msg);
            response.setStatus(status);
        } catch (Exception e) {
            log.info("响应失败", e);
            throw new RuntimeException("响应失败", e);
        }
    }

}
